/*
 * Copyright (C) 2008 Peter Kling
 * 
 * This file is part of 'A Kind of Billiard', a graphical billiard simulation.
 * 
 * 'A Kind of Billiard' is free software: you can redistribute it and/or modify it under the terms of the GNU General
 * Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
 * later version.
 * 
 * 'A Kind of Billiard' is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the
 * implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
 * details.
 * 
 * You should have received a copy of the GNU General Public License along with 'A Kind of Billiard'. If not, see
 * <http://www.gnu.org/licenses/>.
 */

package de.akob.billiard;

import java.util.ArrayList;
import java.util.Timer;
import java.util.TimerTask;

import javax.media.opengl.GL;

import de.akob.mathematics.MathVector;
import de.akob.misc.OpenGLDrawable;
import de.akob.modeling.GLModel;
import de.akob.physics.PhysicsEngine;
import de.akob.physics.SimpleObstacle;

/**
 * Super class for billiard tables. Child classes can use a custom model by setting the display list appropriately. If
 * it is not set, the billiard table will be drawn as a green polygon without feet/borders or similar goodies.
 * Furthermore, an indicator for the direction and force the cue ball will be played with is drawn. Subclasses can
 * adjust this by overwriting drawCueIndicator().
 */
public abstract class BilliardTable implements OpenGLDrawable {
	// update interval: 17 ms ~= 1/60 s
	public static final double          UPDATETIME_DOUBLE_S = 0.017;
	public static final long            UPDATETIME_LONG_MS  = 17;
	
	// geometric table data
	protected ArrayList<SimpleObstacle> planes              = new ArrayList<SimpleObstacle>();
	protected double                    length;
	protected double                    width;
	protected double                    height;
	
	// model for table
	protected GLModel                   model;
	
	// cue indicator
	protected CueIndicator              cueIndicator        = null;
	
	// ball data
	protected BilliardBall              cueBall             = null;
	protected ArrayList<BilliardBall>   balls               = new ArrayList<BilliardBall>();
	protected boolean                   gravityEnabled      = false;
	
	// physics engine
	protected PhysicsEngine             physicsEngine       = new PhysicsEngine();
	protected Timer                     physicsEngineTimer  = new Timer();
	protected TimerTask                 physicsEngineTask   = null;
	
	/**
	 * Constructs a billiard table.
	 */
	public BilliardTable() {
	}
	
	/**
	 * A stroke with the cue using the current cue direction and cue velocity.
	 */
	public void cueStroke() {
		if (cueBall != null)
			cueBall.setVelocity(cueIndicator.getCueStrokeVelocity());
	}
	
	/**
	 * Decrease the velocity the cue ball will be played with.
	 */
	public void decreaseCueVelocity() {
		cueIndicator.decreaseCueVelocity();
	}
	
	/**
	 * Draws the billiard table and its balls.
	 */
	public void draw(GL gl) {
		// draw the table
		if (model == null) {
			gl.glBegin(GL.GL_QUADS);
			gl.glVertex3d(-length / 2., height, width / 2.);
			gl.glVertex3d(length / 2., height, width / 2.);
			gl.glVertex3d(length / 2., height, -width / 2.);
			gl.glVertex3d(-length / 2., height, -width / 2.);
			gl.glEnd();
		} else {
			model.draw(gl);
		}
		
		// draw balls
		gl.glPushMatrix();
		gl.glTranslated(0., height, 0.);
		cueBall.draw(gl);
		for (BilliardBall currBall : balls)
			currBall.draw(gl);
		gl.glPopMatrix();
	}
	
	/**
	 * Returns the cue ball.
	 */
	public BilliardBall getCueBall() {
		return cueBall;
	}
	
	/**
	 * Returns the cue indicator of this table.
	 */
	public CueIndicator getCueIndicator() {
		return cueIndicator;
	}
	
	/**
	 * Returns whether ball gravity is enabled or not.
	 */
	public boolean getGravityEnabled() {
		return gravityEnabled;
	}
	
	/**
	 * Returns the height of the billiard table.
	 */
	public double getHeight() {
		return height;
	}
	
	/**
	 * Increases the velocity the cue ball will be played with.
	 */
	public void increaseCueVelocity() {
		cueIndicator.increaseCueVelocity();
	}
	
	/**
	 * Initializes the billiard table.
	 */
	public void init(GL gl) {
		initBorderPlanes();
		initBalls();
		initBalls_gravity();
		initTable_model(gl);
		initBalls_models(gl);
		initPhysicsEngine();
		cueIndicator = new CueIndicator(this);
		cueIndicator.init(gl);
	}
	
	/**
	 * Initializes the billiard balls. Use this method to load a custom set of balls.
	 */
	protected abstract void initBalls();
	
	/**
	 * Initializes the gravity for billiard balls. Sets the gravity of the balls according to the gravityEnabled member
	 * variable.
	 */
	protected void initBalls_gravity() {
		// if (gravityEnabled == true) {
		// MathVector gravity = Mathematics.mul(Mathematics.getUnitVector(3, 2), -Physics.gravity_moon);
		// if (cueBall != null)
		// cueBall.setAcceleration(gravity);
		// for (PhysicalObject ball : balls)
		// ball.setAcceleration(gravity);
		// } else {
		// if (cueBall != null)
		// cueBall.setAcceleration(0., 0., 0.);
		// for (PhysicalObject ball : balls)
		// ball.setAcceleration(0., 0., 0.);
		// }
	}
	
	/**
	 * Initializes the models for the billiard balls. Overwrite this method to load custom ball models. If this method
	 * is not overwritten, the balls will be drawn using the current OpenGL color.
	 */
	protected void initBalls_models(GL gl) {
	}
	
	/**
	 * Initializes the planes corresponding to the borders of the table. This default implementation will just define 5
	 * border planes (left, right, front, back, bottom). Their will be no 'holes' for the pockets (the geometry depends
	 * on the concrete billiard table implemented). Overwrite this method and add holes at positions suitable for the
	 * implemented billiard table.
	 * <p>
	 * Default: plane0: left; plane1: front; plane2: right; plane3: back, plane4: lower
	 */
	protected void initBorderPlanes() {
		MathVector normal = new MathVector(3);
		
		// plane for left border
		normal.set(1, -1.);
		normal.set(2, 0.);
		normal.set(3, 0.);
		planes.add(new SimpleObstacle(normal, length / 2.));
		
		// plane for front border
		normal.set(1, 0.);
		normal.set(2, 0.);
		normal.set(3, 1.);
		planes.add(new SimpleObstacle(normal, width / 2.));
		
		// plane for right border
		normal.set(1, 1.);
		normal.set(2, 0.);
		normal.set(3, 0.);
		planes.add(new SimpleObstacle(normal, length / 2.));
		
		// plane for back border
		normal.set(1, 0.);
		normal.set(2, 0.);
		normal.set(3, -1.);
		planes.add(new SimpleObstacle(normal, width / 2.));
		
		// plane for lower border
		normal.set(1, 0.);
		normal.set(2, -1.);
		normal.set(3, 0.);
		planes.add(new SimpleObstacle(normal, 0.));
	}
	
	/**
	 * Initializes the physics engine.
	 */
	protected void initPhysicsEngine() {
		physicsEngine.addBody(cueBall);
		for (BilliardBall ball : balls)
			physicsEngine.addBody(ball);
		resume();
	}
	
	/**
	 * Initializes the model for the billiard table. Overwrite this method to load a custom table model. If this method
	 * is not overwritten, the table will be drawn as a green plane in the air.
	 */
	protected void initTable_model(GL gl) {
	}
	
	/**
	 * Pauses the table.
	 */
	public void pause() {
		physicsEngineTask.cancel();
	}
	
	/**
	 * Resets the billiard table. The velocity of the balls is set to zero, so they stop moving. The acceleration of the
	 * balls is set due to the current gravity. The standard implementation will reset the cue indicator, but not the
	 * ball positions (it does not know the exact geometry of the implemented billiard table). Overwrite this method and
	 * reset the ball positions manually.
	 */
	public void reset() {
		cueIndicator.reset();
		pause();
		if (cueBall != null)
			cueBall.setVelocity(0., 0., 0.);
		for (BilliardBall ball : balls)
			ball.setVelocity(0., 0., 0.);
		initBalls_gravity();
		resume();
	}
	
	/**
	 * Resets the cue ball of the billiard table. The velocity of the balls is set to zero, so they stop moving. The
	 * acceleration of the balls is set due to the current gravity. The standard implementation will reset the cue
	 * indicator, but not the cue ball's position (it does not know the exact geometry of the implemented billiard
	 * table). Overwrite this method and reset the position manually.
	 */
	public void resetCueBall() {
		cueIndicator.reset();
		pause();
		if (cueBall != null)
			cueBall.setVelocity(0., 0., 0.);
		for (BilliardBall ball : balls)
			ball.setVelocity(0., 0., 0.);
		initBalls_gravity();
		resume();
	}
	
	/**
	 * Resumes the table.
	 */
	public void resume() {
		physicsEngineTask = new TimerTask() {
			@Override
			public void run() {
				physicsEngine.update(UPDATETIME_DOUBLE_S);
			}
		};
		physicsEngineTimer.scheduleAtFixedRate(physicsEngineTask, 0, Math.round(UPDATETIME_LONG_MS));
	}
	
	/**
	 * Increases the angles for horizontally and vertically rotations of the cue by the given values.
	 */
	public void rotateCue(double angleH, double angleV) {
		cueIndicator.rotateCue(angleH, angleV);
	}
	
	/**
	 * Rotates the cue some degree 'downwards'. Using this method you can rotate the cue such that the ball is played
	 * upwards up to some maximum angle.
	 */
	public void rotateCue_down() {
		cueIndicator.rotateCue_down();
	}
	
	/**
	 * Rotates the cue some degree around the y-axis in mathematical positive sense.
	 */
	public void rotateCue_left() {
		cueIndicator.rotateCue_left();
	}
	
	/**
	 * Rotates the cue some degree around the y-axis in mathematical negative sense.
	 */
	public void rotateCue_right() {
		cueIndicator.rotateCue_right();
	}
	
	/**
	 * Rotates the cue some degree 'upwards'. Using this method you can rotate the cue such that the ball is played
	 * downwards up to some maximum angle.
	 */
	public void rotateCue_up() {
		cueIndicator.rotateCue_up();
	}
	
	/**
	 * Sets the velocity the cue ball will have after the next stroke with the cue.
	 */
	public void setCueVelocity(double value) {
		cueIndicator.setCueVelocity(value);
	}
	
	/**
	 * Switches between enabled/disabled gravity for the balls.
	 */
	public void toggleGravity() {
		gravityEnabled = !gravityEnabled;
		initBalls_gravity();
	}
}
